/**
 * Title: Hyperbolic engine
 * Description: jFamilyTree Engine
 * Version: 2.0.x
 * Copyright: (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 * Update: Mon Mar 10 00:00:00 CEST 2008
 */

package jFamilyTree;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.MediaTracker;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.swing.JPanel;
import javax.swing.SwingWorker;

import jFamilyTree.component.Progress;
import jFamilyTree.component.Statusbar;
import jFamilyTree.component.Toolbar;
import jFamilyTree.control.Controller;
import jFamilyTree.control.Mounter;
import jFamilyTree.control.Controller.MatchMode;
import jFamilyTree.control.Controller.MatchScope;
import jFamilyTree.core.AbstractLayerOut;
import jFamilyTree.core.Complex;
import jFamilyTree.core.LayerOut;
import jFamilyTree.core.Weigher;
import jFamilyTree.model.Tree;
import jFamilyTree.model.IEdge;
import jFamilyTree.model.INode;
import jFamilyTree.model.IProvider;
import jFamilyTree.model.IProviderContext;
import jFamilyTree.model.Location;
import jFamilyTree.model.Model;
import jFamilyTree.model.MountPoint;
import jFamilyTree.model.Settings;
import jFamilyTree.view.View;

/**
 * jFamilyTree's main panel. Call either init() to feed the data
 * 
 * @author Bernard Bou
 */
public class Widget extends JPanel implements IWidget, IProviderContext
{
	private static final long serialVersionUID = 020000L;

	// B E H A V I O U R

	/**
	 * Loading data on separate thread
	 */
	static private boolean debug = false;

	/**
	 * Loading data on separate thread
	 */
	static private final boolean threaded = true;

	/**
	 * XML is parsed by DOM parser
	 */
	static private final boolean fromDom = true;

	/**
	 * Animate on start flag
	 */
	static private final boolean animateOnStart = true;

	// V E R S I O N

	/**
	 * Version
	 */
	private static final String theVersion = "provider";

	// C O N T E X T

	/**
	 * Context for operations (provides some glue to the context, application)
	 * 
	 * @see IContext
	 */
	private final IContext theContext;

	// M O D E L - V I E W - C O N T R O L L E R

	/**
	 * Model
	 */
	private Model theModel;

	/**
	 * View
	 */
	private View theView;

	/**
	 * Controller
	 */
	private final Controller theController;

	// D A T A . P R O V I D E R

	/**
	 * Provider
	 */
	private IProvider theProvider;

	// A G E N T S

	/**
	 * Weigher
	 */
	private final Weigher theWeigher;

	/**
	 * Provider
	 */
	private final AbstractLayerOut theLayerOut;

	// C O M P O N E N T S

	/**
	 * Toolbar
	 */
	private Toolbar theToolbar;

	/**
	 * Status bar
	 */
	private Statusbar theStatusbar;

	/**
	 * Progress
	 */
	private Progress theProgress;

	// I M A G E . M A N A G E M E N T

	/**
	 * Media tracker
	 */
	private MediaTracker theMediaTracker;

	/**
	 * Name-to-image map, used to void loading twice the same images
	 */
	private Hashtable<String, Image> theImages;

	/**
	 * Images base Url
	 */
	private URL theImagesBase;

	// C O N S T R U C T O R

	/**
	 * Constructor
	 * 
	 * @param thisContext
	 *        context
	 */
	public Widget(IContext thisContext)
	{
		theContext = thisContext;

		// media tracker
		theMediaTracker = new MediaTracker(this);

		// layout
		setLayout(new BorderLayout());

		// first components
		theProgress = new Progress();
		add(theProgress, BorderLayout.CENTER);

		// size
		setPreferredSize(new Dimension(800, 600));

		// controller
		theController = new Controller();
		theController.connect(this);

		// weigher
		theWeigher = new Weigher();

		// layer out
		theLayerOut = new LayerOut();
		theController.connect(theLayerOut);
	}

	// I N I T

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#init()
	 */
	public void init()
	{
		// source
		String thisSource = getParameter("doc");
		if (thisSource == null || thisSource.isEmpty())
			thisSource = getParameter("xml");

		// provider
		String thisProviderName = getParameter("provider");

		// init
		init(thisProviderName, thisSource);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#init(java.lang.String, java.lang.String)
	 */
	public void init(String thisProviderName, final String thisSource)
	{
		if (thisProviderName == null)
			thisProviderName = fromDom ? "jFamilyTree.provider.xml.dom.Provider" : "jFamilyTree.provider.xml.fast.Provider";

		if ("true".equals(getParameter("debug")))
			debug = true;
		if (debug)
			System.out.println("PROVIDER=" + thisProviderName);

		// make provider
		theProvider = makeProvider(thisProviderName);
		if (theProvider == null)
		{
			putProgress("Cannot load provider <" + thisProviderName + ">", true);
			return;
		}

		// pass context reference to provider, so that it can access some services
		theProvider.connect(this);

		// init
		initProcess(thisSource);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#reinit(java.lang.String)
	 */
	public void reinit(final String thisSource)
	{
		if (theProvider == null)
		{
			putProgress("Null provider " + theProvider, true);
			return;
		}

		// init
		initProcess(thisSource);
	}

	/**
	 * Init (typically called by embedding applet's init()). Data source and data provider have not yet been determined.
	 * 
	 * @param thisSource
	 *        source (anything the provider will make sense of)
	 */
	protected void initProcess(final String thisSource)
	{
		// further (possibly lengthy) loading is done on separate thread
		if (!threaded)
			init(theProvider, thisSource);
		else
		{
			SwingWorker<Void, Void> thisWorker = new SwingWorker<Void, Void>()
			{
				@Override
				protected Void doInBackground() throws Exception
				{
					try
					{
						init(theProvider, thisSource);
					}
					catch (Throwable e)
					{
						System.err.println("Worker thread: " + e.toString());
						e.printStackTrace();
					}
					return null;
				}

				@Override
				protected void done()
				{
				}
			};
			thisWorker.execute();
		}
	}

	/**
	 * Init from provider and source
	 * 
	 * @param thisProvider
	 *        data provider
	 * @param thisSource
	 *        data source
	 */
	protected void init(IProvider thisProvider, String thisSource)
	{
		if (debug)
			System.out.println("SOURCE=" + thisSource);
		putProgress("Loading " + thisSource, false);
		Model thisModel = thisProvider.makeModel(thisSource);
		if (thisModel == null)
		{
			putProgress("Provider " + thisProvider.getClass().getCanonicalName() + " returned null model from source <" + thisSource + ">", true);
			return;
		}
		putProgress("Loaded " + thisSource, false);

		// load model
		init(thisModel);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#init(jFamilyTree.model.Model)
	 */
	public void init(Model thisModel)
	{
		if (thisModel == null)
		{
			putProgress("Null model", true);
			putMessage("Aborting");
			return;
		}

		// model/view/controller
		theModel = thisModel;

		// view
		theView = new View();

		// connect view and controller to each other
		theController.connect(theView);
		theView.connect(theController);

		// connect both controller and view to model
		theView.connect(thisModel);
		theController.connect(thisModel);

		// connect view to layerout
		theView.connect(theLayerOut);

		// initiate image loading
		putProgress("Images loading...", false);
		loadImages();
		putProgress("Images loaded.", false);

		// enforce settings
		theLayerOut.apply(theModel.theSettings);
		theView.apply(theModel.theSettings);

		// weigh model
		putProgress("Weighing nodes ...", false);
		theWeigher.weigh(thisModel.theTree.getRoot());

		// lay out model
		putProgress("Laying out ...", false);
		theLayerOut.layout(thisModel.theTree.getRoot());

		// initial transform
		theView.applyInitialTransform();

		// remove all previous components (possibly progress or previous view and tools)
		removeAll();

		// status bar
		if (theModel.theSettings.theHasStatusbarFlag != null && theModel.theSettings.theHasStatusbarFlag)
		{
			theStatusbar = new Statusbar();
			add(theStatusbar, BorderLayout.SOUTH);
		}

		// toolbar
		if (theModel.theSettings.theHasToolbarFlag != null && theModel.theSettings.theHasToolbarFlag)
		{
			boolean hasTooltip = theModel.theSettings.theHasToolTipFlag != null ? theModel.theSettings.theHasToolTipFlag : true;
			boolean toolTipDisplaysContent = theModel.theSettings.theToolTipDisplaysContentFlag != null ? theModel.theSettings.theToolTipDisplaysContentFlag : true;
			boolean edgesHyperbolic = theModel.theSettings.theHyperbolicEdgesFlag != null ? theModel.theSettings.theHyperbolicEdgesFlag : true;
			boolean focusOnHover = theModel.theSettings.theFocusOnHoverFlag != null ? theModel.theSettings.theFocusOnHoverFlag : true;
			theToolbar = new Toolbar(theController, hasTooltip, toolTipDisplaysContent, edgesHyperbolic, focusOnHover);
			add(theToolbar, Toolbar.isVertical ? BorderLayout.EAST : BorderLayout.NORTH);
		}

		// show view
		add(theView, BorderLayout.CENTER);
		validate();

		// animate
		INode thisFocus = getFocusNode();
		if (!animateOnStart)
		{
			// no animation
			theView.moveToCenter(thisFocus);
		}
		else
		{
			// animation to focus
			if ((theModel.theSettings.theXMoveTo != null && theModel.theSettings.theXMoveTo < 1.) || (theModel.theSettings.theYMoveTo != null && theModel.theSettings.theYMoveTo < 1.))
			{
				// move required
				Complex thisTo = new Complex(theModel.theSettings.theXMoveTo == null ? 0. : theModel.theSettings.theXMoveTo, theModel.theSettings.theYMoveTo == null ? 0. : theModel.theSettings.theYMoveTo);
				if (thisTo.abs2() > 1.)
					thisTo.normalize().multiply(.9);
				theView.animateTo(thisFocus, thisTo, false);
			}
			else
			{
				theView.animateToCenter(thisFocus, false);
			}
		}
	}

	// N A V I G A T I O N

	/**
	 * Mount a source at node
	 * 
	 * @param thisMountingNode
	 *        mounting node
	 * @param thisSource
	 *        mounted source
	 */
	public synchronized void mount(INode thisMountingNode, String thisSource)
	{
		putInformation("Mount", thisSource, false);

		if (theProvider == null)
		{
			putInformation("Mount", "Null provider", false);

			// make provider
			theProvider = makeProvider("jFamilyTree.provider.xml.dom.Provider");
			if (theProvider == null)
			{
				putInformation("Mount", "Cannot load provider jFamilyTree.provider.xml.dom.Provider", true);
				return;
			}

			// pass context reference to provider, so that it can access some services
			theProvider.connect(this);
		}

		// make model
		Tree thisTree = theProvider.makeTree(thisSource, false);
		if (thisTree == null)
		{
			putInformation("Mount", "failed to get " + thisSource, false);
			return;
		}

		// extract subroot
		INode thisMountedRoot = thisTree.getRoot();

		// images
		loadImages(thisMountedRoot);

		// ensure edge list is non null
		if (theModel.theTree.getEdges() == null)
			theModel.theTree.setEdges(new ArrayList<IEdge>());

		// graft nodes
		if (!Mounter.graft(thisMountingNode, thisMountedRoot, theModel.theTree.getEdges(), thisTree.getEdges()))
		{
			putInformation("Mount", "failed mounting", false);
			return;
		}

		// weigh
		theWeigher.weigh(thisMountedRoot);
		thisMountingNode.setChildrenWeight(thisMountedRoot.getChildrenWeight());
		thisMountingNode.setMinWeight(thisMountedRoot.getMinWeight());

		// compute locations : layout
		MountPoint.Mounting thisMountingPoint = (MountPoint.Mounting) thisMountingNode.getMountPoint();
		theLayerOut.layout(thisMountedRoot, thisMountingNode.getLocation().hyper.center0, thisMountingPoint.theHalfWedge, thisMountingPoint.theOrientation);

		// notify view
		theView.mount(thisMountedRoot);
	}

	/**
	 * Unmount at mountpoint node
	 * 
	 * @param thisMountedNode
	 *        mounted node
	 */
	public synchronized void umount(INode thisMountedNode)
	{
		putInformation("UnMount", "", false);

		// model
		INode thisMountingNode = Mounter.prune(thisMountedNode, theModel.theTree.getEdges());
		if (thisMountingNode == null)
		{
			putInformation("Unmount", "failed unmounting", false);
			return;
		}

		// compute locations : copy
		Location thisMountedNodeLocation = thisMountedNode.getLocation();
		Location thisMountingNodeLocation = thisMountingNode.getLocation();
		thisMountingNodeLocation.hyper.clone(thisMountedNodeLocation.hyper);

		// notify view
		theView.umount(thisMountingNode);
	}

	// A C C E S S

	/**
	 * Get version
	 * 
	 * @return version
	 */
	public String getVersion()
	{
		return theVersion;
	}

	/**
	 * Get context
	 * 
	 * @return context
	 */
	public IContext getContext()
	{
		return theContext;
	}

	// P R O V I D E R - F A C T O R Y

	/**
	 * Make provider
	 * 
	 * @param thisProviderName
	 *        provider name
	 * @return provider
	 */
	private IProvider makeProvider(String thisProviderName)
	{
		try
		{
			Class<?> thisClass = Class.forName(thisProviderName);
			Class<?>[] theseArgsClass = new Class[] {};
			Object[] theseArgs = new Object[] {};

			Constructor<?> thisConstructor = thisClass.getConstructor(theseArgsClass);
			Object thisInstance = thisConstructor.newInstance(theseArgs);
			return (IProvider) thisInstance;
		}
		catch (ClassNotFoundException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		catch (NoSuchMethodException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		catch (IllegalAccessException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		catch (InstantiationException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		catch (IllegalArgumentException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		catch (InvocationTargetException e)
		{
			System.err.println("Provider factory: " + e.toString());
		}
		return null;
	}

	// P R O V I D E R . C O N T E X T

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.model.IProviderContext#getParameter(java.lang.String)
	 */
	public String getParameter(String thisName)
	{
		// delegate to context
		return theContext.getParameter(thisName);
	}

	// U R L . F A C T O R Y

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.model.IProviderContext#makeURL(java.lang.String)
	 */
	public URL makeURL(String thisSource)
	{
		if (thisSource == null)
		{
			if (debug)
				System.out.println("MAKEURL= null (null source)");
			return null;
		}

		// try to consider it well-formed full-fledged url
		try
		{
			URL thisUrl = new URL(thisSource);
			if (debug)
				System.out.println("URL=" + new URL(thisSource).toString());
			return thisUrl;
		}
		catch (MalformedURLException e)
		{
		}

		// default to source relative to a base
		try
		{
			URL thisBaseUrl = getBase();
			if (debug)
				System.out.println("BASE URL=" + thisBaseUrl.toString());
			URL thisUrl = new URL(thisBaseUrl, thisSource);
			if (debug)
				System.out.println("URL=" + thisUrl.toString());
			return thisUrl;
		}
		catch (MalformedURLException e)
		{
		}
		if (debug)
			System.out.println("URL= null (fail)");
		return null;
	}

	/**
	 * Get base url
	 * 
	 * @return base url
	 */
	private URL getBase()
	{
		return theContext.getBase();
	}

	/**
	 * Get image Url
	 * 
	 * @param thisImageSource
	 *        image source
	 * @return image url
	 */
	private URL makeImageURL(String thisImageSource)
	{
		try
		{
			return new URL(theImagesBase, thisImageSource);
		}
		catch (MalformedURLException thisException)
		{
		}
		return null;
	}

	// I M A G E . L O A D E R

	private void loadImages()
	{
		theImages = new Hashtable<String, Image>();
		theImagesBase = theContext.getImagesBase();
		if (debug)
			System.out.println("IMAGEBASE URL=" + theImagesBase.toString());
		loadImages(theModel.theTree.getRoot());
		loadImages(theModel.theTree.getEdges());
		loadTopImages(theModel.theSettings);
		try
		{
			theMediaTracker.waitForAll();
		}
		catch (InterruptedException thisException)
		{
			System.err.println("Loading images interrupted: " + thisException.toString());
		}
	}

	/**
	 * Load node and node's children's images
	 * 
	 * @param thisNode
	 *        starting node
	 */
	private void loadImages(INode thisNode)
	{
		if (thisNode == null)
			return;

		// node image
		String thisSource = thisNode.getImageFile();
		if (thisSource != null)
		{
			// cache lookup
			Image thisImage = theImages.get(thisSource);
			if (thisImage == null)
			{
				// cache miss
				thisImage = loadImage(thisSource);
			}

			// store image in the node
			thisNode.setImage(thisImage);
		}

		// edge image
		thisSource = thisNode.getEdgeImageFile();
		if (thisSource != null)
		{
			// cache lookup
			Image thisImage = theImages.get(thisSource);
			if (thisImage == null)
			{
				// cache miss
				thisImage = loadImage(thisSource);
			}
			thisNode.setEdgeImage(thisImage);
		}

		// recurse on mounting node obfuscated by mounted node)
		MountPoint thisMountPoint = thisNode.getMountPoint();
		if (thisMountPoint != null && thisMountPoint instanceof MountPoint.Mounted)
		{
			MountPoint.Mounted thisMountedPoint = (MountPoint.Mounted) thisMountPoint;
			INode theMountingNode = thisMountedPoint.theMountingNode;
			loadImages(theMountingNode);
		}

		// recurse children
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
			for (INode thisChild : thisNode.getChildren())
				loadImages(thisChild);
	}

	/**
	 * Load edges' images
	 * 
	 * @param thisEdgeList
	 *        edge list
	 */
	private void loadImages(List<IEdge> thisEdgeList)
	{
		if (thisEdgeList != null)
		{
			for (IEdge thisEdge : thisEdgeList)
				loadImage(thisEdge);
		}
	}

	/**
	 * Load edge image
	 * 
	 * @param thisEdge
	 *        edge
	 */
	private void loadImage(IEdge thisEdge)
	{
		if (thisEdge == null)
			return;
		String thisSource = thisEdge.getImageFile();
		if (thisSource != null)
		{
			// cache lookup
			Image thisImage = theImages.get(thisSource);
			if (thisImage == null)
			{
				// cache miss
				thisImage = loadImage(thisSource);
			}
			thisEdge.setImage(thisImage);
		}
	}

	/**
	 * Load top images
	 * 
	 * @param theseSettings
	 *        settings
	 */
	private void loadTopImages(Settings theseSettings)
	{
		Image thisBackgroundImage = loadImage(theseSettings.theBackgroundImage);
		Image thisDefaultNodeImage = loadImage(theseSettings.theDefaultNodeImage);
		Image thisDefaultTreeEdgeImage = loadImage(theseSettings.theDefaultTreeEdgeImage);
		Image thisDefaultEdgeImage = loadImage(theseSettings.theDefaultEdgeImage);
		theView.setImages(thisBackgroundImage, thisDefaultNodeImage, thisDefaultTreeEdgeImage, thisDefaultEdgeImage);
	}

	/**
	 * Load image given its source
	 * 
	 * @param thisSource
	 *        source
	 * @return image
	 */
	private Image loadImage(String thisSource)
	{
		if (thisSource == null)
			return null;

		URL thisUrl = makeImageURL(thisSource);
		if (thisUrl != null)
		{
			if (debug)
				System.out.println("IMAGE=" + thisUrl);

			// delegate image loading to context
			Image thisImage = theContext.getImage(thisUrl);
			if (thisImage != null)
			{
				// cache image
				theImages.put(thisSource, thisImage);
				// track it
				theMediaTracker.addImage(thisImage, thisSource.hashCode());
				return thisImage;
			}
		}
		return null;
	}

	// F O C U S . N O D E

	/**
	 * Get node to initially receive focus
	 * 
	 * @return node
	 */
	private INode getFocusNode()
	{
		// focus node
		String thisFocusNodeId = null;
		if (theModel.theSettings.theFocus != null)
			thisFocusNodeId = theModel.theSettings.theFocus;

		// anchor of document base
		URL thisBase = theContext.getHtmlDocumentBase();
		if (thisBase != null)
			thisFocusNodeId = thisBase.getRef();

		// animate
		INode thisFocus = null;
		if (thisFocusNodeId == null)
			thisFocus = theModel.theTree.getRoot();
		else
		{
			thisFocus = theModel.theTree.findNodeById(theModel.theTree.getRoot(), thisFocusNodeId);

			// default to root if not found
			if (thisFocus == null)
				thisFocus = theModel.theTree.getRoot();
		}
		return thisFocus;
	}

	// P R O G R E S S

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.model.IProviderContext#putProgress(java.lang.String)
	 */
	public void putProgress(String thisString, boolean fail)
	{
		theProgress.put(thisString, fail);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.model.IProviderContext#putMessage(java.lang.String)
	 */
	public void putMessage(String thisString)
	{
		theContext.showStatus(thisString);
	}

	// S T A T U S

	/**
	 * Get target
	 * 
	 * @return target
	 */
	public String getTarget()
	{
		if (theStatusbar != null)
			return theStatusbar.get();
		return null;
	}

	/**
	 * Put information
	 * 
	 * @param thisHeader
	 *        header
	 * @param thisContent
	 *        content
	 */
	public void putInformation(String thisHeader, String thisContent, boolean isLink)
	{
		if (theStatusbar == null)
			return;
		theStatusbar.put(thisHeader, thisContent, isLink ? Statusbar.PutType.LINK : Statusbar.PutType.INFO);
	}

	// JavaScript

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#focus(java.lang.String)
	 */
	public void focus(String thisNodeId)
	{
		theController.focus(thisNodeId);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#match(java.lang.String, java.lang.String, java.lang.String)
	 */
	public String match(String thisTargetString, String thisScopeString, String thisModeString)
	{
		MatchScope thisScope = MatchScope.LABEL;
		if (thisScopeString != null)
		{
			if (thisScopeString.equals("label"))
				thisScope = MatchScope.LABEL;
			else if (thisScopeString.equals("content"))
				thisScope = MatchScope.CONTENT;
			else if (thisScopeString.equals("link"))
				thisScope = MatchScope.LINK;
			else if (thisScopeString.equals("id"))
				thisScope = MatchScope.ID;
		}

		MatchMode thisMode = MatchMode.EQUALS;
		if (thisModeString != null)
		{
			if (thisModeString.equals("equals"))
				thisMode = MatchMode.EQUALS;
			else if (thisModeString.equals("startswith"))
				thisMode = MatchMode.STARTSWITH;
			else if (thisModeString.equals("includes"))
				thisMode = MatchMode.INCLUDES;
		}
		return theController.match(thisTargetString, thisScope, thisMode);
	}

	/*
	 * (non-Javadoc)
	 * @see jFamilyTree.IWidget#link(java.lang.String, java.lang.String)
	 */
	public void link(String thisUrlString, String thisUrlTarget)
	{
		theController.linkTo(thisUrlString, thisUrlTarget);
	}
}
